﻿//-----------------------------------------------------------------------
// <copyright company="Microsoft Corporation">
//     Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------

#define WPF_3_5_SP1
//#define WPF_3_5
//#define WPF_3_0_SP2
//#define WPF_3_0_SP1
//#define WPF_3_0

#if WPF_3_5_SP1
#define WPF_3_5
#define WPF_3_0SP2
#endif

#if WPF_3_5
#define WPF_3_0SP1
#endif

#if WPF_3_0SP2
#define WPF_3_0SP1
#endif

#if WPF_3_0SP1
#define WPF_3_0
#endif

namespace Microsoft.DevDiv.Wpf.Samples
{
    using System;
    using System.Threading;
    using System.Windows;
    using System.Windows.Markup;
    using System.Windows.Media;
    using System.Windows.Media.Imaging;
    using System.Windows.Threading;

    /// <summary>
    /// Markup extension for for async image loading
    /// </summary>
    /// <remarks>
    /// Experiment to see what moving blocking calls off the UI thread looks like with Rx.Net
    /// Pretty much all of the Rx.Net stuff is in AsyncImageSourceOperations
    /// The rest of it is to support an object model that plays nice with Xaml
    /// </remarks>
    /// <example>
    /// <![CDATA[
    /// <Image Source={local:AsyncImageSource http://example.com/image.png, BusySource=busy.png, ErrorSource=error.png}" />
    /// or in a template
    /// <DataTemplate TargetType="{x:Type sys:Uri}">
    ///     <!-- The Uri to load is obtained from Image.DataContext -->
    ///     <Image Source={local:AsyncImageSource BusySource=busy.png, ErrorSource=error.png}" />
    /// </DataTemplate>
    /// ]]>
    /// </example>
    [MarkupExtensionReturnType(typeof(ImageSource))]
    public class AsyncImageSourceExtension : DeferredMarkupExtension<ImageSource>
    {
        public AsyncImageSourceExtension()
        {
        }

        public AsyncImageSourceExtension(Uri sourceUri) :
            this()
        {
            this.Source = sourceUri;
        }

        /// <summary>
        /// Uri of image to download and decode
        /// </summary>
        public Uri Source
        {
            get;
            set;
        }

        /// <summary>
        /// Image source to display while the primary image is being downloaded and decoded
        /// </summary>
        public ImageSource BusySource
        {
            get;
            set;
        }

        /// <summary>
        /// Image source to display if there is a download or decode error
        /// </summary>
        public ImageSource ErrorSource
        {
            get;
            set;
        }

        protected override ImageSource BeginProvideValue(IServiceProvider serviceProvider, IProvideValueTargetSetter<ImageSource> setter)
        {
            Uri baseUri = null;
            IUriContext uriContextService = GetService<IUriContext>(serviceProvider);
            if (uriContextService != null)
            {
                baseUri = uriContextService.BaseUri;
            }

            DependencyObject targetObject = null;
            IProvideValueTarget provideValueTarget = GetService<IProvideValueTarget>(serviceProvider);
            if (provideValueTarget != null)
            {
                targetObject = provideValueTarget.TargetObject as DependencyObject;
            }

            Uri imageUri = ResolveUri(targetObject, baseUri, this.Source);

            if (imageUri != null && imageUri.IsAbsoluteUri)
            {
                AsyncScheduler.Post(() => SetFrozenImageSourceFromUri(imageUri, setter));
            }

            return this.BusySource;
        }

        private object SetFrozenImageSourceFromUri(Uri uri, IProvideValueTargetSetter<ImageSource> setter)
        {
            Action<ImageSource> setValueAndDispose =
                x =>
                {
                    setter.SetValue(x);
                    setter.Dispose();
                };

            ImageSource errorSource = this.ErrorSource;
            
            Action<Exception> setErrorAndDispose =
                (e) =>
                {
                    setter.SetValue(errorSource);
                    setter.Dispose();
                };

           ImageSourceOperations.GetFrozenImageSourceFromUri(uri, setValueAndDispose, setErrorAndDispose);

           return null;
        }

        /// <summary>
        /// Resolve relative Uri's
        /// </summary>
        /// <param name="dataContextSource">If sourceUri is null try to substitute dataContextSource.DataContext</param>
        /// <returns></returns>
        private static Uri ResolveUri(DependencyObject dataContextSource, Uri baseUri, Uri sourceUri)
        {
            Uri result;

            if (sourceUri == null && dataContextSource != null)
            {
                sourceUri = dataContextSource.GetValue(FrameworkElement.DataContextProperty) as Uri;
            }

            if (baseUri != null && baseUri.IsAbsoluteUri && sourceUri != null)
            {
                result = new Uri(baseUri, sourceUri);
            }
            else
            {
                result = sourceUri;
            }

            return result;
        }

        private static T GetService<T>(IServiceProvider serviceProvider)
            where T : class
        {
            return (serviceProvider != null) ? (serviceProvider.GetService(typeof(T)) as T) : null;
        }
    }

    public static class ImageSourceOperations
    {
        public static void GetFrozenImageSourceFromUri(
            Uri imageUri,
            Action<ImageSource> completedCallback,
            Action<Exception> failedCallback)
        {
            if (imageUri == null)
            {
                completedCallback(null);
            }

            BitmapDecoder decoder;
            Exception decodeException;
            try
            {
                decoder = BitmapDecoder.Create(imageUri, BitmapCreateOptions.None, BitmapCacheOption.OnLoad);
                decodeException = null;
            }
            catch (InvalidOperationException e)
            {
                decoder = null;
                decodeException = e;
            }

            if (decoder != null && decodeException == null && decoder.IsDownloading)
            {
                RegisterDownloadCompleted(decoder, completedCallback, failedCallback);
                RegisterDownloadFailed(decoder, failedCallback);
            }
            else
            {
                if (decodeException != null)
                {
                    failedCallback(decodeException);
                }
                else
                {
                    HandleImageAvailbable(decoder, completedCallback, failedCallback);
                }
            }
        }

        private static void RegisterDownloadCompleted(
            BitmapDecoder decoder, 
            Action<ImageSource> callback,
            Action<Exception> failedCallback)
        {
            EventHandler result = null;

            result = 
                (object sender, EventArgs e) =>
                {
                    BitmapDecoder senderAsDecoder = sender as BitmapDecoder;
                    if (senderAsDecoder != null)
                    {
                        senderAsDecoder.DownloadCompleted -= result;
                        HandleImageAvailbable(senderAsDecoder, callback, failedCallback);
                    }
                };

            decoder.DownloadCompleted += result;
        }

        private static void RegisterDownloadFailed(BitmapDecoder decoder, Action<Exception> failedCallback)
        {
            EventHandler<ExceptionEventArgs> result = null;

            result =
                (object sender, ExceptionEventArgs e) =>
                {
                    BitmapDecoder senderAsDecoder = sender as BitmapDecoder;
                    if (senderAsDecoder != null)
                    {
                        senderAsDecoder.DownloadFailed -= result;
                        failedCallback(e.ErrorException);
                    }
                };

            decoder.DownloadFailed += result;
        }

        private static void HandleImageAvailbable(BitmapDecoder decoder, Action<ImageSource> completedCallback, Action<Exception> failedCallback)
        {
            if (decoder != null && decoder.Frames.Count > 0)
            {
                ImageSource frame = decoder.Frames[0];
                frame.Freeze();
                completedCallback(frame);
            }
            else
            {
                failedCallback(null);
            }
        }
    }

#if (WPF_3_0 || WPF_3_5 || WPF_3_5_SP1)
    public delegate void Action();
#endif

    public static class AsyncScheduler
    {
        public static void Post(Action callback)
        {
#if (WPF_3_0 || WPF_3_5 || WPF_3_5_SP1)
            // We must ensure there is a running dispatcher on the thread that executes the callback
            // Because of a bug in BitmapDecoder (WPF 3.5 and earlier) 
            // BitmapDecoder does not notifying on download completed in non dispatcher threads (e.g ThreadPool)            

            Dispatcher dispatcher = _dispatcher;

            if (dispatcher == null || dispatcher.HasShutdownStarted)
            {
                Thread thread = new Thread(ThreadStartCallback)
                {
                    IsBackground = true
                };

                thread.Start(callback);
            }
            else
            {
                dispatcher.BeginInvoke(DispatcherPriority.Normal, callback);
            }
#else
            ThreadPool.QueueUserWorkItem(x => callback());
#endif
        }

        [STAThread]
        private static void ThreadStartCallback(object arg)
        {
            Dispatcher dispatcher = Dispatcher.CurrentDispatcher;
            dispatcher.BeginInvoke(DispatcherPriority.Normal, (Action)arg);
            _dispatcher = dispatcher;
            Dispatcher.Run();
        }

        private static Dispatcher _dispatcher;
    }
}
